Popular Searches
Popular Course Categories
Popular Courses

Implementing Many-to-Many Relationships

Implementing Many-to-Many Relationships

Hibernate Relationships and Query Language

Implementing Many-to-Many Relationships in Hibernate

Course: Advanced Java Training

Topic: Implementing Many-to-Many Relationships

Course Link: Advanced Java Training Course – JustAcademy

Demo Link: Register for a Course Demo

Website: JustAcademy Official Website


1. Introduction to Many-to-Many Relationships

A many-to-many relationship is a type of association in which multiple records from one entity can be associated with multiple records from another entity. Hibernate supports this relationship using the @ManyToMany annotation and a database join table.

For example, a student can enroll in multiple courses, and each course can have multiple students. Similarly, an employee can work on several projects, and each project can involve multiple employees.

In a relational database, a many-to-many relationship is normally represented using a third table called a join table or association table. This table stores foreign keys referencing the primary keys of the two associated tables.

Real-World Examples

  • Students and courses: one student can enroll in many courses, and one course can have many students.
  • Employees and projects: an employee can work on multiple projects, and each project can involve multiple employees.
  • Authors and books: one author may write multiple books, and a book may have multiple authors.
  • Products and categories: a product may belong to multiple categories, and a category may contain multiple products.
  • Users and roles: a user may have multiple roles, and each role may be assigned to multiple users.

2. Learning Objectives

After completing this topic, learners should be able to:

  • Explain many-to-many relationships in relational databases and Hibernate.
  • Understand the purpose of a join table.
  • Implement unidirectional many-to-many relationships.
  • Implement bidirectional many-to-many relationships.
  • Use @ManyToMany, @JoinTable, and @JoinColumn.
  • Understand owning and inverse sides of an association.
  • Manage associated entities using Hibernate transactions.
  • Configure appropriate cascade operations.
  • Perform CRUD operations with many-to-many relationships.
  • Identify common mapping errors and performance problems.
  • Determine when a join table should be represented as a separate entity.

3. Understanding the Many-to-Many Relationship

Consider a training application with two entities: Student and Course. A student can register for several courses, while a course can have several students.

A direct foreign key in either the Student or Course table cannot adequately represent this relationship because each side may be associated with multiple records from the other side. A join table solves this problem by storing individual student-course associations.

Database Structure

STUDENT
-------------------
id (Primary Key)
name
email

COURSE
-------------------
id (Primary Key)
course_name
description

STUDENT_COURSE
-------------------
student_id (Foreign Key)
course_id  (Foreign Key)

Relationship Diagram

+----------------+        +---------------------+
|    STUDENT     |        |       COURSE        |
+----------------+        +---------------------+
| id             |        | id                  |
| name           |        | course_name         |
| email          |        | description         |
+----------------+        +---------------------+
         |                           |
         |                           |
         +------ STUDENT_COURSE ------+
                    |
             student_id
             course_id

The STUDENT_COURSE table connects students and courses. Each row represents one association between a particular student and a particular course.

4. What Is a Join Table?

A join table is a separate database table that connects two entities by storing references to their primary keys. In a many-to-many mapping, it normally contains at least two foreign-key columns.

For example, the following table records which students have enrolled in which courses.

student_id course_id
1 101
1 102
2 101
3 103

In this example:

  • Student 1 is enrolled in courses 101 and 102.
  • Student 2 is enrolled in course 101.
  • Student 3 is enrolled in course 103.
  • Course 101 has more than one student.

A database can use a composite primary key or unique constraint on (student_id, course_id) to prevent duplicate associations. Both columns should normally be foreign keys to their respective tables.

5. Understanding the @ManyToMany Annotation

The @ManyToMany annotation is provided by Jakarta Persistence and is used by Hibernate to map a many-to-many association between entity classes.

Basic Syntax

@ManyToMany
private Set<Course> courses = new HashSet<>();

This example indicates that the entity contains a collection of associated Course entities.

Important Mapping Annotations

Annotation Purpose
@ManyToMany Defines a many-to-many association between entities.
@JoinTable Configures the join table used to store the association.
@JoinColumn Specifies a foreign-key column in the join table.
@Entity Marks a Java class as a persistent entity.
@Table Specifies the database table mapped to an entity.
@Id Defines the primary key of an entity.
@GeneratedValue Configures primary-key generation.

6. Types of Many-to-Many Relationships

Hibernate supports two common forms of many-to-many associations.

6.1 Unidirectional Many-to-Many

In a unidirectional association, only one entity contains a collection reference to the other entity. For example, Student can access its courses, but Course does not contain a collection of students.

Student ------> Course

6.2 Bidirectional Many-to-Many

In a bidirectional association, both entities contain references to each other. Student can access its courses, and Course can access its students.

Student <------> Course

A bidirectional association has one owning side and one inverse side. The owning side controls updates to the join table, while the inverse side uses the mappedBy attribute to refer to the owning-side property.

7. Implementing a Unidirectional Many-to-Many Relationship

In this example, a Student can be associated with multiple Course entities. The Course entity does not contain a collection of students.

7.1 Create the Course Entity

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "courses")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String courseName;

    private String description;

    public Course() {
    }

    public Long getId() {
        return id;
    }

    public String getCourseName() {
        return courseName;
    }

    public void setCourseName(String courseName) {
        this.courseName = courseName;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

7.2 Create the Student Entity

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    @ManyToMany(cascade = {
        CascadeType.PERSIST,
        CascadeType.MERGE
    })
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();

    public Student() {
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Set<Course> getCourses() {
        return courses;
    }

    public void addCourse(Course course) {
        courses.add(course);
    }

    public void removeCourse(Course course) {
        courses.remove(course);
    }
}

7.3 Understanding the Mapping

  • @ManyToMany indicates that one student can have multiple courses.
  • @JoinTable(name = "student_course") defines the association table.
  • joinColumns specifies the foreign key referencing the Student entity.
  • inverseJoinColumns specifies the foreign key referencing the Course entity.
  • Set<Course> stores the courses associated with a student and prevents duplicate elements according to the Java collection's equality rules.
  • CascadeType.PERSIST propagates persistence to new associated courses.
  • CascadeType.MERGE propagates merge operations to associated courses.

In this mapping, Student is the owning side. Hibernate uses the Student's course collection to update the join table.

8. Saving Entities in a Unidirectional Relationship

When cascade persist is configured, Hibernate can persist new Course entities while persisting their associated Student.

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

public class StudentService {

    private final SessionFactory sessionFactory;

    public StudentService(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public void createStudentWithCourses() {
        try (Session session = sessionFactory.openSession()) {
            Transaction transaction = session.beginTransaction();

            try {
                Course javaCourse = new Course();
                javaCourse.setCourseName("Advanced Java");
                javaCourse.setDescription("Java and Hibernate");

                Course sqlCourse = new Course();
                sqlCourse.setCourseName("SQL Fundamentals");
                sqlCourse.setDescription("Relational database basics");

                Student student = new Student();
                student.setName("Rahul Sharma");
                student.setEmail("[email protected]");

                student.addCourse(javaCourse);
                student.addCourse(sqlCourse);

                session.persist(student);

                transaction.commit();
            } catch (RuntimeException e) {
                if (transaction.isActive()) {
                    transaction.rollback();
                }
                throw e;
            }
        }
    }
}

Step-by-Step Explanation

  1. Create the Java course object.
  2. Create the SQL course object.
  3. Create a Student object and assign its basic information.
  4. Add both courses to the student's course collection.
  5. Begin a Hibernate transaction.
  6. Call session.persist(student).
  7. Hibernate persists the new courses because cascade persist is configured.
  8. Hibernate writes the associations to the join table.
  9. Commit the transaction to complete the database work.

If any persistence operation fails, the transaction should be rolled back when possible, and the affected Session should be closed and discarded.

9. Implementing a Bidirectional Many-to-Many Relationship

A bidirectional many-to-many association allows both entities to navigate the relationship. In the student-course example, Student stores a collection of courses, while Course stores a collection of students.

9.1 Student Entity — Owning Side

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    @ManyToMany(cascade = {
        CascadeType.PERSIST,
        CascadeType.MERGE
    })
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();

    public Student() {
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Set<Course> getCourses() {
        return courses;
    }

    public void addCourse(Course course) {
        courses.add(course);
        course.getStudents().add(this);
    }

    public void removeCourse(Course course) {
        courses.remove(course);
        course.getStudents().remove(this);
    }
}

9.2 Course Entity — Inverse Side

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "courses")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String courseName;

    private String description;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();

    public Course() {
    }

    public Long getId() {
        return id;
    }

    public String getCourseName() {
        return courseName;
    }

    public void setCourseName(String courseName) {
        this.courseName = courseName;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Set<Student> getStudents() {
        return students;
    }
}

9.3 Understanding the Bidirectional Mapping

  • Student is the owning side because it defines @JoinTable.
  • Course is the inverse side because it declares @ManyToMany(mappedBy = "courses").
  • The value courses in mappedBy refers to the Java property in Student.
  • The owning side determines changes to the association table.
  • The helper methods update both Java collections to keep the object graph synchronized.

Do not set only the inverse-side collection and assume that Hibernate will update the join table. Always update the owning side as well, preferably through helper methods.

10. Understanding @JoinTable

The @JoinTable annotation defines the database table that stores the many-to-many relationship.

@JoinTable(
    name = "student_course",
    joinColumns = @JoinColumn(name = "student_id"),
    inverseJoinColumns = @JoinColumn(name = "course_id")
)

Important Attributes

Attribute Purpose
name Specifies the join-table name.
joinColumns Defines join-table columns referencing the entity on the owning side.
inverseJoinColumns Defines join-table columns referencing the associated entity.
schema Can specify the database schema containing the join table.
catalog Can specify the database catalog where supported.
uniqueConstraints Can define uniqueness constraints when schema generation is used.

Example Join Table Structure

CREATE TABLE student_course (
    student_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

This SQL illustrates a common schema design. Adapt the data types and constraint syntax to the database in use. A composite primary key prevents the same student-course association from being inserted more than once.

11. Understanding Cascade Operations

Cascade operations allow entity lifecycle operations to propagate from one entity to associated entities. They are useful when the lifecycle of the associated entities is intentionally connected to the lifecycle of the parent entity.

Cascade Type Purpose
PERSIST Propagates persist operations to associated entities.
MERGE Propagates merge operations to associated entities.
REMOVE Propagates entity removal to associated entities.
REFRESH Propagates refresh operations.
DETACH Propagates detach operations.
ALL Includes all the standard cascade operations listed above.

Recommended Cascade Approach

@ManyToMany(cascade = {
    CascadeType.PERSIST,
    CascadeType.MERGE
})
private Set<Course> courses = new HashSet<>();

For many-to-many relationships, avoid cascading REMOVE or using CascadeType.ALL without careful consideration. Associated entities are often shared by multiple parent entities. Deleting a Student should normally remove the corresponding join-table rows, not delete the Course records themselves.

12. Adding a Relationship Between Existing Entities

Many applications associate entities that already exist in the database. For example, a student may select an existing Java course from a course catalogue.

try (Session session = sessionFactory.openSession()) {
    Transaction transaction = session.beginTransaction();

    try {
        Student student = session.find(Student.class, 1L);
        Course course = session.find(Course.class, 101L);

        if (student == null || course == null) {
            throw new IllegalArgumentException(
                "Student or course was not found"
            );
        }

        student.addCourse(course);

        transaction.commit();
    } catch (RuntimeException e) {
        if (transaction.isActive()) {
            transaction.rollback();
        }
        throw e;
    }
}

This example assumes the bidirectional mapping from Section 9. The helper method adds the course to the Student's owning-side collection and updates the inverse-side collection. Because both entities are managed by the current Session, Hibernate can synchronize the association at flush or commit.

13. Removing a Relationship Without Deleting Entities

Removing a course from a student's collection normally removes the association row from the join table. It should not delete the Course entity itself.

try (Session session = sessionFactory.openSession()) {
    Transaction transaction = session.beginTransaction();

    try {
        Student student = session.find(Student.class, 1L);
        Course course = session.find(Course.class, 101L);

        if (student != null && course != null) {
            student.removeCourse(course);
        }

        transaction.commit();
    } catch (RuntimeException e) {
        if (transaction.isActive()) {
            transaction.rollback();
        }
        throw e;
    }
}

In the bidirectional mapping, the helper method updates both collections. The Course record remains in the database unless it is explicitly removed through a separate operation.

14. Performing CRUD Operations

14.1 Create

Create new Student and Course objects, associate them, and persist them within a transaction. Cascade persist can be used when new associated entities should be saved automatically.

Student student = new Student();
student.setName("Priya");
student.setEmail("[email protected]");

Course course = new Course();
course.setCourseName("Hibernate Fundamentals");

student.addCourse(course);

session.persist(student);

This snippet assumes that Student owns the association and has cascade persist configured.

14.2 Read

Student student = session.find(Student.class, 1L);

if (student != null) {
    for (Course course : student.getCourses()) {
        System.out.println(course.getCourseName());
    }
}

Depending on the fetch strategy, accessing the collection may trigger a database query. Read associated data while the required persistence context is available, or fetch the data explicitly through a query.

14.3 Update

Student student = session.find(Student.class, 1L);
Course course = session.find(Course.class, 102L);

if (student != null && course != null) {
    student.addCourse(course);
}

This example adds another course to the student's associations. For a managed Student entity, Hibernate can detect changes to its collection and update the join table when the transaction is synchronized.

14.4 Delete an Association

Student student = session.find(Student.class, 1L);
Course course = session.find(Course.class, 102L);

if (student != null && course != null) {
    student.removeCourse(course);
}

This removes the relationship between the Student and Course. It does not explicitly delete either entity.

14.5 Delete an Entity

Student student = session.find(Student.class, 1L);

if (student != null) {
    session.remove(student);
}

When a Student is removed, Hibernate removes its join-table associations as part of the entity removal process. The associated Course entities should generally remain because they may be linked to other students. The actual outcome also depends on database constraints and mapping configuration.

15. Transaction Management for Many-to-Many Relationships

Use Hibernate transactions to group related database changes into a single unit of work. This is particularly important when creating entities and updating join-table rows together.

try (Session session = sessionFactory.openSession()) {
    Transaction transaction = session.beginTransaction();

    try {
        Student student = session.find(Student.class, 1L);
        Course course = session.find(Course.class, 101L);

        if (student == null || course == null) {
            throw new IllegalArgumentException(
                "Required entity was not found"
            );
        }

        student.addCourse(course);

        transaction.commit();
    } catch (RuntimeException e) {
        if (transaction.isActive()) {
            transaction.rollback();
        }
        throw e;
    }
}

The transaction ensures that the join-table update participates in the same database transaction as the other operations. If a persistence failure occurs, roll back when possible and close/discard the affected Session.

16. Fetching Strategies and Performance

Fetch strategies determine when associated entities are loaded. For many-to-many associations, unnecessary eager fetching can cause large result sets, repeated joins, and performance problems.

Lazy Fetching

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
    name = "student_course",
    joinColumns = @JoinColumn(name = "student_id"),
    inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();

Lazy fetching defers loading the associated collection until it is accessed, where supported by the persistence provider and mapping. The collection generally needs an open persistence context when it is initialized.

Fetch Join Example

List<Student> students = session.createQuery(
    "select distinct s from Student s " +
    "left join fetch s.courses",
    Student.class
).getResultList();

This query fetches students together with their associated courses. The distinct keyword helps express the intent to return distinct Student entities when a join produces multiple rows per student. Be careful when combining collection fetch joins with pagination because the database rows and logical entity results do not have a simple one-to-one correspondence.

Performance Best Practices

  • Prefer lazy fetching for many-to-many collections unless eager loading is specifically required.
  • Use fetch joins or entity graphs when the application needs associated data in a particular query.
  • Watch for N+1 query problems when iterating over collections.
  • Avoid loading large collections when only a small subset is required.
  • Consider batch fetching for repeated collection loads.
  • Use database indexes on join-table foreign-key columns where appropriate.
  • Inspect generated SQL and test with realistic data volumes.

17. Using Set, List, and Collection

Java collection types affect how associations behave in memory and may influence how Hibernate manages join-table changes.

Collection Type Characteristics Considerations
Set Prevents duplicate elements according to equals() and hashCode(). Useful when each association should be unique; implement equality carefully for entities.
List Allows repeated elements and maintains an in-memory sequence. Do not assume list order is persisted unless an ordering or index mapping is configured.
Collection Provides a general collection interface. Choose it when the application does not need Set or List-specific behavior.

For a simple many-to-many association where the same student should not be enrolled in the same course twice, a Set is often a natural choice. Also enforce uniqueness in the database because Java collection rules alone do not guarantee database integrity.

18. When Should You Use a Join Entity Instead?

A direct @ManyToMany mapping is convenient when the association contains only the two foreign keys. However, real applications often need additional information about the relationship itself.

For example, a student-course association might need:

  • Enrollment date.
  • Course completion status.
  • Marks or grade.
  • Enrollment type.
  • Payment status.
  • Last updated timestamp.

These fields belong to the association, not solely to Student or Course. In such situations, create an intermediate entity such as Enrollment and map two relationships using @ManyToOne and @OneToMany.

Example Enrollment Entity

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;

@Entity
@Table(name = "enrollments")
public class Enrollment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "student_id", nullable = false)
    private Student student;

    @ManyToOne(optional = false)
    @JoinColumn(name = "course_id", nullable = false)
    private Course course;

    private LocalDate enrollmentDate;

    private String status;

    public Enrollment() {
    }

    public Long getId() {
        return id;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }

    public Course getCourse() {
        return course;
    }

    public void setCourse(Course course) {
        this.course = course;
    }

    public LocalDate getEnrollmentDate() {
        return enrollmentDate;
    }

    public void setEnrollmentDate(LocalDate enrollmentDate) {
        this.enrollmentDate = enrollmentDate;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

In this model, Enrollment represents the relationship explicitly. Student and Course can each have a collection of Enrollment entities through corresponding @OneToMany(mappedBy = "...") mappings if bidirectional navigation is required.

Advantages of a Join Entity

  • Allows additional attributes to be stored for each association.
  • Provides explicit control over the association's lifecycle.
  • Makes it easier to query, update, and audit individual relationships.
  • Avoids cascading entity deletion across shared many-to-many associations.
  • Supports more flexible constraints and business rules.

19. Common Errors and Troubleshooting

19.1 Join Table Is Not Updated

Problem: The association appears in Java but is missing from the database.

Cause: The application modified only the inverse side of a bidirectional relationship.

Solution: Update the owning side and use helper methods to keep both collections synchronized.

19.2 Duplicate Join Table Rows

Problem: The same entity pair appears more than once in the join table.

Cause: The database does not enforce uniqueness, or the application allows duplicate associations.

Solution: Use a Set where appropriate and enforce a composite primary key or unique constraint on the pair of foreign keys.

19.3 Cascade Delete Removes Shared Data

Problem: Deleting one entity attempts to delete another entity that is still referenced elsewhere.

Cause: Cascade REMOVE or CascadeType.ALL was applied to a shared many-to-many association.

Solution: Avoid cascading REMOVE across shared entities. Remove the association or delete the entity only when its independent lifecycle and business rules allow it.

19.4 LazyInitializationException

Problem: The application accesses a collection after the Session has closed.

Solution: Fetch the required data within the transaction or use a fetch join, entity graph, or DTO query that returns the necessary information.

19.5 Unexpected SQL Statements

Problem: Hibernate executes more SQL than expected when a collection changes.

Cause: Collection semantics and the association mapping can influence how join-table rows are deleted and reinserted.

Solution: Inspect generated SQL, choose an appropriate collection type, and consider a join entity if the relationship requires fine-grained lifecycle control.

19.6 TransientObjectException

Problem: Hibernate reports that an unsaved transient entity is referenced.

Cause: A new associated entity was not persisted, and the mapping does not cascade the necessary operation.

Solution: Persist the entity explicitly or configure cascade persist when the lifecycle relationship justifies it.

20. Best Practices for Many-to-Many Relationships

  • Use @ManyToMany when the association is simple and does not need its own attributes.
  • Define a clear owning side for bidirectional associations.
  • Use @JoinTable to specify meaningful join-table and column names.
  • Use foreign-key constraints and uniqueness constraints to protect data integrity.
  • Keep both sides synchronized with helper methods.
  • Avoid cascading REMOVE across shared entities.
  • Use lazy fetching by default for potentially large collections.
  • Inspect generated SQL when modifying collections.
  • Use a join entity when the association needs extra fields or lifecycle management.
  • Perform database changes within transactions.
  • Close Sessions reliably and discard a Session after a persistence failure.
  • Test relationship creation, removal, duplicate prevention, and concurrent updates.

21. Practical Project: Student Course Enrollment System

Project Objective: Build a Java application using Hibernate to manage students, courses, and student-course associations.

Project Requirements

  • Create Student and Course entity classes.
  • Implement a bidirectional many-to-many relationship.
  • Configure a join table named student_course.
  • Use a Set to manage course associations without duplicate elements in memory.
  • Write a service method to create a student and associate courses.
  • Write a method to enroll an existing student in an existing course.
  • Write a method to remove an enrollment without deleting the course.
  • Retrieve a student with the associated courses.
  • Handle missing records and transaction failures.
  • Inspect the generated SQL and verify the join-table contents.

Optional Enhancement

Extend the project by replacing the direct many-to-many association with an Enrollment entity containing enrollment date, payment status, and course completion status.

22. Practice Exercises

  1. Create Student and Course entities with a unidirectional many-to-many relationship.
  2. Convert the mapping to bidirectional and use mappedBy.
  3. Configure a custom join table and explicit join-column names.
  4. Save a new student with two new courses using cascade persist.
  5. Associate an existing student with an existing course.
  6. Remove a course association and confirm that the Course record remains.
  7. Prevent duplicate student-course pairs through database constraints.
  8. Compare the SQL generated when using Set and List collections.
  9. Test a lazy collection after the Session is closed and resolve the problem.
  10. Create an Enrollment entity and store additional association details.

23. Interview Questions and Answers

Q1. What is a many-to-many relationship in Hibernate?

Answer: It is an association where multiple instances of one entity can be related to multiple instances of another entity, usually through a join table.

Q2. Which annotation is used for a many-to-many relationship?

Answer: The @ManyToMany annotation from Jakarta Persistence.

Q3. Why is a join table required?

Answer: A join table stores the foreign-key references that connect records from both entity tables.

Q4. What is the purpose of @JoinTable?

Answer: It defines the join table and its join columns for a many-to-many association.

Q5. What is the difference between joinColumns and inverseJoinColumns?

Answer: joinColumns references the owning entity, while inverseJoinColumns references the associated entity.

Q6. What is the difference between unidirectional and bidirectional many-to-many relationships?

Answer: Unidirectional relationships allow navigation from one entity to the other. Bidirectional relationships allow navigation from both entities.

Q7. What does mappedBy do?

Answer: It identifies the owning-side association property from the inverse side of a bidirectional relationship.

Q8. Which side owns a bidirectional many-to-many relationship?

Answer: The side that defines the physical join-table mapping, usually with @JoinTable, is the owning side.

Q9. Why should CascadeType.REMOVE generally be avoided in many-to-many relationships?

Answer: The associated entities may be shared by other entities. Cascading removal could delete shared data or cause foreign-key constraint violations.

Q10. Why is Set often used for many-to-many relationships?

Answer: A Set prevents duplicate elements according to Java equality rules, which is useful when duplicate associations are not allowed. Database constraints are still needed for integrity.

Q11. How do you remove a many-to-many association without deleting either entity?

Answer: Remove the relationship from the owning-side collection and synchronize the inverse side when applicable. Hibernate then updates the join table.

Q12. What causes LazyInitializationException?

Answer: It can occur when a lazy collection is accessed after its persistence context has closed and the collection has not been initialized.

Q13. When should a join table be mapped as a separate entity?

Answer: When the association requires extra attributes, auditing, independent lifecycle management, or more explicit control over updates and deletion.

Q14. How can duplicate join-table records be prevented?

Answer: Use an appropriate Set in the Java model and enforce a composite primary key or unique constraint on the pair of foreign-key columns.

Q15. What happens when a Student entity is deleted?

Answer: Hibernate normally removes the association rows connected to that Student. The associated Course entities should remain unless separately deleted according to the application's rules.

24. Quick Revision Checklist

  • @ManyToMany maps a many-to-many association.
  • A join table normally stores the two foreign keys.
  • @JoinTable specifies the join table and its join columns.
  • joinColumns references the owning entity.
  • inverseJoinColumns references the associated entity.
  • mappedBy identifies the owning-side property.
  • Update the owning side to ensure relationship changes are persisted.
  • A Set can help prevent duplicate elements in memory.
  • Use database constraints to enforce referential integrity and uniqueness.
  • Avoid cascading REMOVE across shared many-to-many entities.
  • Use lazy fetching and explicit fetch plans where appropriate.
  • Use an intermediate entity when the association has additional attributes.
  • Manage database modifications within transactions.

25. Conclusion

Implementing many-to-many relationships is an important Hibernate skill for applications where records from two entities can be associated in multiple ways. The @ManyToMany annotation, @JoinTable, and mappedBy make it possible to represent these associations while maintaining a clear entity model.

Reliable implementations require careful handling of relationship ownership, database constraints, cascade operations, fetching strategies, and transaction boundaries. When an association needs additional fields or its own lifecycle, representing the join table as an entity is often the more flexible design.

Further Learning

Explore the Advanced Java Training Course at JustAcademy to continue learning Hibernate entity relationships and persistence concepts.

Register for a course demo to learn more about the training.

Visit the JustAcademy official website for additional courses and learning resources.

Technical Reference: Hibernate ORM User Guide – Many-to-Many Associations

Previous Next
whatsapp